package jp.co.nes.awssupport.business.logic;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectSummary;

import jp.co.nes.awssupport.access.dao.SforceManageDao;
import jp.co.nes.awssupport.access.utility.IdUtil;
import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.bean.AwsKeyBean;
import jp.co.nes.awssupport.common.bean.ContractInformationBean;
import jp.co.nes.awssupport.common.bean.CreditCouponAssignedBean;
import jp.co.nes.awssupport.common.bean.CreditCouponBean;
import jp.co.nes.awssupport.common.bean.CreditCouponHistoryBean;
import jp.co.nes.awssupport.common.bean.IamUserBean;
import jp.co.nes.awssupport.common.bean.TaxAssignedBean;
import jp.co.nes.awssupport.common.bean.UsageHistoryBean;
import jp.co.nes.awssupport.common.constant.CreditCouponHistoryKind;
import jp.co.nes.awssupport.common.utility.AppInfoUtil;
import jp.co.nes.awssupport.common.utility.AwsKmsHelper;
import jp.co.nes.awssupport.common.utility.AwsS3Helper;
import jp.co.nes.awssupport.common.utility.DateUtil;
import jp.co.nes.awssupport.common.utility.YamlUtil;

/**
 * 契約情報管理です。
 */
public class ContractInformationManage {

    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(ContractInformationManage.class);

    /** 契約情報リスト(テスト用) */
    private static List<ContractInformationBean> ciListForTest;

    /** 契約情報リスト */
    private List<ContractInformationBean> ciList;

    /** 処理対象年月 */
    private YearMonth targetYearMonth;

    /** バケット名 */
    private String bucketName;

    /** Sforce反映を実施する連結アカウントIDリスト */
    private List<String> saveLinkedAccountIdList = new ArrayList<>();

    /**
     * 処理対象年月を引数にもつコンストラクタです。
     *
     * @param targetYearMonth 処理対象年月
     */
    public ContractInformationManage(YearMonth targetYearMonth) {
        this.targetYearMonth = targetYearMonth;
        String category = "ContractInformation";
        this.bucketName = AppInfoUtil.getValAndEmptyChk(category, "BucketName");
        if (logger.isDebugEnabled()) {
            logger.debug(category + ",BucketName=" + bucketName);
        }
    }

    /**
     * 契約情報からアクセスキー、シークレットキーを取得して、AwsKeyを返却します。
     *
     * @param ciBean 契約情報
     * @return AwsKey
     */
    public static AwsKeyBean getAwsKey(ContractInformationBean ciBean) {
        if (ciBean == null) {
            throw new CommonException("ContractInformationBean is null");
        }

        AwsKeyBean akBean = null;
        IamUserBean iuBean = ciBean.getApiIamUser();
        if (iuBean == null) {
            throw new CommonException("IamUserBean is null");
        } else {
            logger.info("getAwsKey: create helper start");
            String decryptAccessKey = null;
            String decryptSecurityKey = null;
            AwsKmsHelper helper = new AwsKmsHelper();
            String encryptAccessKey = iuBean.getAccessKey();
            String encryptSecurityKey = iuBean.getSecurityKey();
            if (!StringUtils.isEmpty(encryptAccessKey)) {
                decryptAccessKey = helper.decrypt(encryptAccessKey);
            }
            if (!StringUtils.isEmpty(encryptSecurityKey)) {
                decryptSecurityKey = helper.decrypt(encryptSecurityKey);
            }
            akBean = new AwsKeyBean(decryptAccessKey, decryptSecurityKey);
            logger.info("getAwsKey: create helper end");
        }
        return akBean;
    }

    /**
     * 契約情報リスト(テスト用)を設定します。<br>
     * JUnitによるテストの中で使用するメソッドです。<br>
     * JUnitによるテスト以外で使用しないでください。
     *
     * @param ciList 契約情報リスト(テスト用)
     */
    public static void set(List<ContractInformationBean> ciList) {
        ciListForTest = ciList;
    }

    /**
     * 契約情報をダウンロードします。
     *
     * @return ダウンロードした契約情報の連結アカウントIDリスト
     */
    public List<String> download() {
        logger.info("download start");

        String category = "ContractInformation";
        String encoding = AppInfoUtil.getVal(category, "Encoding", "UTF-8");
        String contentType = AppInfoUtil.getVal(category, "ContentType", "application/octet-stream");
        if (logger.isDebugEnabled()) {
            logger.debug(category + ",Encoding=" + encoding);
            logger.debug(category + ",ContentType=" + contentType);
        }

        logger.info("ContractInformation download(sforce) start");
        ciList = getContractInformationList(targetYearMonth);
        if (logger.isDebugEnabled()) {
            logger.debug("cbiList size=" + ciList.size());
        }
        logger.info("ContractInformation download(sforce) end");

        logger.info("s3 get list start");
        String targetYearMonthValue = DateUtil.toString(targetYearMonth);
        AwsS3Helper helperForObjectListing = createAwsS3Helper();
        ObjectListing objectListing = helperForObjectListing.listObjects(bucketName, targetYearMonthValue);
        logger.info("s3 get list end");

        logger.info("s3 delete object start");
        List<S3ObjectSummary> s3ObjectSummaryList = objectListing.getObjectSummaries();
        s3ObjectSummaryList.stream().parallel().forEach(s3ObjectSummary -> {
            // parallelでs3上の契約情報を削除する。
            AwsS3Helper helper = createAwsS3Helper();
            helper.deleteObject(bucketName, s3ObjectSummary.getKey());
        });
        logger.info("s3 delete object end");

        logger.info("s3 put object start");
        ciList.stream().parallel().forEach(bean -> {
            // parallelで契約情報をGZIP圧縮して、s3へアップロードする。

            // GZIP圧縮
            byte[] beanByteArray = YamlUtil.convertBeanToYamlBytes(bean);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            try (OutputStream outputStream = new GZIPOutputStream(byteArrayOutputStream)) {
                outputStream.write(beanByteArray);
            } catch (IOException e) {
                throw new CommonException(e);
            }

            // s3へアップロード
            byte[] compressionByteArray = byteArrayOutputStream.toByteArray();
            try (InputStream inputStream = new BufferedInputStream(new ByteArrayInputStream(compressionByteArray))) {
                String objectKey = targetYearMonthValue + "/" + bean.getLinkedAccountId() + ".yaml.gz";
                long contentLength = compressionByteArray.length;

                AwsS3Helper helper = createAwsS3Helper();
                helper.putObject(bucketName, objectKey, inputStream, encoding, contentLength, contentType);
            } catch (IOException e) {
                throw new CommonException(e);
            }
        });
        logger.info("s3 put object end");

        List<String> dlList = ciList.stream().map(ciBean -> ciBean.getLinkedAccountId()).collect(Collectors.toList());
        logger.info("download end");

        return dlList;
    }

    /**
     * 指定された連結アカウントIDに対応する契約情報を返却します。<br>
     * 存在しない場合はnullを返却します。
     *
     * @param linkedAccountId 連結アカウントID
     * @return 契約情報
     */
    public ContractInformationBean get(String linkedAccountId) {
        load();
        List<ContractInformationBean> filterList = ciList.stream().filter(bean -> {
            String beanLinkedAccountId = bean.getLinkedAccountId();
            if (beanLinkedAccountId == null) {
                throw new CommonException("linkedAccountId of ContractInformationBean is null");
            }
            if (beanLinkedAccountId.equals(linkedAccountId)) {
                return true;
            }
            return false;
        }).collect(Collectors.toList());

        ContractInformationBean ciBean = null;
        int size = filterList.size();
        if (size == 1) {
            ciBean = filterList.get(0);
        } else if (size > 1) {
            throw new CommonException("filter result size > 1(size=" + size + ")");
        }
        return ciBean;
    }

    /**
     * 引数の内容からForce.com連携するクレジット履歴を作成・追加します。
     *
     * @param bean クレジット適用
     */
    public void add(CreditCouponAssignedBean bean) {
        String linkedAccountId = bean.getLinkedAccountId();
        if (!saveLinkedAccountIdList.contains(linkedAccountId)) {
            saveLinkedAccountIdList.add(linkedAccountId);
        }

        String creditCouponId = bean.getCreditCouponId();

        ContractInformationBean ciBean = get(linkedAccountId);
        if (ciBean == null) {
            throw new CommonException("contract information not exist(linkedAccountId=" + linkedAccountId + ")");
        }
        List<CreditCouponBean> ccList = ciBean.getCreditCouponList();
        CreditCouponBean ccBean = ccList.stream()
                .filter(ccb -> ccb.getCreditCouponId().equals(creditCouponId))
                .findFirst()
                .orElseThrow(() -> new CommonException("CreditCoupon is null(creditCouponId=" + creditCouponId + ")"));
        List<CreditCouponHistoryBean> cchList = ccBean.getCreditCouponHistoryList();
        if (cchList == null) {
            cchList = new ArrayList<>();
            ccBean.setCreditCouponHistoryList(cchList);
        }

        String yearMonthValue = Integer.toString(bean.getYearMonth());
        CreditCouponHistoryKind kind = CreditCouponHistoryKind.getKindFromLocalValue(bean.getCreditCouponHistoryKind());
        String kindSforceValue = kind.getSforceValue();
        BigDecimal creditCouponAmountUSDollar = bean.getCreditCouponAmountUSDollar();
        String name = yearMonthValue + "_" + kindSforceValue;
        String exId = IdUtil.createCreditCouponHistoryExId(creditCouponId, yearMonthValue, kind);
        BigDecimal creditCouponHistoryValue = creditCouponAmountUSDollar.multiply(new BigDecimal("-1"));

        CreditCouponHistoryBean cchBean = new CreditCouponHistoryBean();
        cchBean.setName(name);
        cchBean.setExId(exId);
        cchBean.setCreditCouponHistoryKind(kindSforceValue);
        cchBean.setCreditCouponHistoryYearMonth(yearMonthValue);
        cchBean.setCreditCouponHistoryValue(creditCouponHistoryValue);
        cchList.add(cchBean);
    }

    /**
     * 引数の内容からForce.com連携する利用実績を作成・追加します。<br>
     * 税金適用の課税対象額、税額は、小数第4位で四捨五入して小数第3位にした値の合計を、
     * 小数第1位で四捨五入して整数にした値とします。
     *
     * @param bean 税金適用
     */
    public void add(TaxAssignedBean bean) {
        String linkedAccountId = bean.getLinkedAccountId();
        if (!saveLinkedAccountIdList.contains(linkedAccountId)) {
            saveLinkedAccountIdList.add(linkedAccountId);
        }

        String yearMonthValue = Integer.toString(bean.getYearMonth());

        // 税金適用の課税対象額は、小数第1位で四捨五入して整数にした値が設定されている前提
        // →連結アカウントID, 年月では全て同じ値が入っている前提なので、
        //   連結アカウントID, 年月で税額を集計する際、min関数を使用して取得した値が入っている想定。
        // 税金適用の税額は、小数第4位で四捨五入して小数第3位にした値の合計を
        // 小数第1位で四捨五入して整数にした値が設定されている前提
        BigDecimal targetAmount = bean.getTargetAmount();
        BigDecimal taxAmount = bean.getTaxAmount();
        long billingAmount = targetAmount.add(taxAmount).setScale(0, RoundingMode.HALF_UP).longValueExact();
        long billingAmountWithoutTax = targetAmount.setScale(0, RoundingMode.HALF_UP).longValueExact();

        UsageHistoryBean uhBean = new UsageHistoryBean();
        uhBean.setName(yearMonthValue);
        uhBean.setExId(IdUtil.createUsageHistoryExId(linkedAccountId, yearMonthValue));
        uhBean.setBillingAmount(billingAmount);
        uhBean.setBillingAmountWithoutTax(billingAmountWithoutTax);

        ContractInformationBean ciBean = get(linkedAccountId);
        if (ciBean == null) {
            throw new CommonException("contract information not exist(linkedAccountId=" + linkedAccountId + ")");
        }
        ciBean.setUsageHistoryBean(uhBean);
    }

    /**
     * 追加されているクレジット、利用実績をForce.comに保存します。
     */
    public void saveCreditCouponAndUsageHistory() {
        load();
        SforceManageDao.saveAll(false, getSaveCiList(), targetYearMonth);
    }

    /**
     * 利用実績をForce.comに保存します。
     */
    public void saveUsageHistory() {
        load();
        SforceManageDao.saveUsageHistory(getSaveCiList());
    }

    /**
     * 契約情報リストを返却します。
     *
     * @param targetYearMonth 処理対象年月
     * @return 契約情報リスト
     */
    protected List<ContractInformationBean> getContractInformationList(YearMonth targetYearMonth) {
        String category = "ContractInformation";
        String keyArn = AppInfoUtil.getValAndEmptyChk(category, "KeyArn");

        List<ContractInformationBean> list = SforceManageDao.getContractInformationList(null, targetYearMonth);

        logger.info("getContractInformationList: create helper start");
        list.stream().parallel().forEach(ciBean -> {
            // parallelでアクセスキーとシークレットキーを暗号化する。
            AwsKmsHelper helper = createAwsKmsHelper();
            IamUserBean aiuBean = ciBean.getApiIamUser();
            String decryptAccessKey = aiuBean.getAccessKey();
            if (!StringUtils.isEmpty(decryptAccessKey)) {
                String encryptAccessKey = helper.encrypt(keyArn, decryptAccessKey);
                aiuBean.setAccessKey(encryptAccessKey);
            }
            String decryptSecurityKey = aiuBean.getSecurityKey();
            if (!StringUtils.isEmpty(decryptSecurityKey)) {
                String encryptSecurityKey = helper.encrypt(keyArn, decryptSecurityKey);
                aiuBean.setSecurityKey(encryptSecurityKey);
            }
        });
        logger.info("getContractInformationList: create helper end");
        return list;
    }

    /**
     * Sforce反映を実施する契約情報リストを返却します。
     *
     * @return Sforce反映を実施する契約情報リスト
     */
    private List<ContractInformationBean> getSaveCiList() {
        List<ContractInformationBean> saveCiList = new ArrayList<>();
        for (ContractInformationBean ciBean : ciList) {
            String linkedAccountId = ciBean.getLinkedAccountId();
            if (saveLinkedAccountIdList.contains(linkedAccountId)) {
                saveCiList.add(ciBean);
            }
        }
        return saveCiList;
    }

    /**
     * ダウンロードした契約情報をロードします。
     */
    private void load() {
        if (ciListForTest != null) {
            // 契約情報リスト(テスト用)が設定されている場合は、契約情報リストにテスト用の内容を設定し、処理を抜ける。
            ciList = ciListForTest;
            return;
        }

        if (ciList == null) {
            ciList = new ArrayList<>();

            logger.info("s3 get list start");
            String targetYearMonthValue = DateUtil.toString(targetYearMonth);
            AwsS3Helper helperForObjectListing = createAwsS3Helper();
            ObjectListing objectListing = helperForObjectListing.listObjects(bucketName, targetYearMonthValue);
            logger.info("s3 get list end");

            logger.info("s3 get object start");
            Class<ContractInformationBean> cls = ContractInformationBean.class;
            List<S3ObjectSummary> s3ObjectSummaryList = objectListing.getObjectSummaries();
            s3ObjectSummaryList.stream().parallel().forEach(s3ObjectSummary -> {
                // parallelでs3上の契約情報を取得する。
                AwsS3Helper helper = createAwsS3Helper();
                S3Object s3Object = helper.getObject(bucketName, s3ObjectSummary.getKey());
                try (InputStream inputStream = new GZIPInputStream(s3Object.getObjectContent())) {
                    synchronized (ciList) {
                        ContractInformationBean ciBean = YamlUtil.convertYamlInputStreamToBean(inputStream, cls);
                        ciList.add(ciBean);
                    }
                } catch (IOException e) {
                    throw new CommonException(e);
                }
            });

            if (logger.isDebugEnabled()) {
                logger.debug("contractInformationList.size()=" + ciList.size());
            }
            logger.info("s3 get object end");
        }
    }

    /**
     * kmsヘルパーを生成します。
     *
     * @return kmsヘルパー
     */
    protected AwsKmsHelper createAwsKmsHelper() {
        return new AwsKmsHelper();
    }

    /**
     * s3ヘルパーを生成します。
     *
     * @return s3ヘルパー
     */
    protected AwsS3Helper createAwsS3Helper() {
        return new AwsS3Helper();
    }
}
